// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections.Generic;

namespace SnapObjects.Data
{
    internal class PocoModelAccessor<TModel> : IModelAccessor<TModel>
    {
        protected readonly IModelFactory<TModel> _factory;
        protected readonly IReadOnlyDictionary<string, IPropertyAccessor> _accessors;

        public PocoModelAccessor(IModelFactory<TModel> factory, Type modelType)
        {
            _factory = factory;
            this.ModelMeta = this.CreateModelMeta(modelType);

            _accessors = PropertyAccessorFactory.Instance.GetPropertyAccessors(modelType);

            this.PropertyCount = _accessors.Count;
        }

        protected virtual PocoModelMeta CreateModelMeta(Type modelType)
        {
            return ModelMetaManager.GetModelMeta(modelType);
        }

        public IModelMeta ModelMeta { get; }

        public int IndexOf(string name)
        {
            return this.ModelMeta.IndexOf(name);
        }

        public string NameOf(int index)
        {
            return this.ModelMeta.NameOf(index);
        }

        public int PropertyCount { get; }

        public virtual object GetValue(TModel model, int index)
        {
            return this.GetValue(model, this.NameOf(index));
        }

        public virtual object GetValue(TModel model, string name)
        {
            var accessor = _accessors[name];

            if (accessor == null)
            {
                throw new InvalidOperationException("Property not exists, property: " + name);
            }

            return accessor.GetValue(model);
        }

        public virtual void SetValue(TModel model, int index, object value)
        {
            this.SetValue(model, this.NameOf(index), value);
        }

        public virtual void SetValue(TModel model, string name, object value)
        {
            var accessor = _accessors[name];

            if (accessor == null)
            {
                throw new InvalidOperationException("Property not exists, property: " + name);
            }

            accessor.SetValue(model, value);
        }

        public virtual TModel ToModel(
            IModelEntry<TModel> modelEntry,
            bool current = true)
        {
            var model = _factory.Create();

            foreach (var name in modelEntry.Properties)
            {
                _accessors.TryGetValue(name, out var accessor);

                if (accessor != null)
                {
                    var value = current ? modelEntry.GetCurrentValue(name) :
                                            modelEntry.GetOriginalValue(name);

                    accessor.SetValue(model, value);
                }
            }

            return model;
        }

        public virtual TModel Clone(TModel model)
        {
            var item = _factory.Create();

            foreach (var property in _accessors)
            {
                var value = property.Value.GetValue(model);

                property.Value.TrySetValue(item, value);
            }

            return item;
        }

        public void CopyModel(TModel toModel, TModel fromModel)
        {
            for (var i = 0; i < this.PropertyCount; i++)
            {
                this.SetValue(toModel, i, this.GetValue(fromModel, i));
            }
        }

        public bool ValueEquals(TModel model1, TModel model2)
        {
            if (model1 == null && model2 == null)
            {
                return true;
            }

            if (model1 == null || model2 == null)
            {
                return false;
            }

            if (model1.GetType() == model2.GetType())
            {
                for (var i = 0; i < this.PropertyCount; i++)
                {
                    if (!ModelHelper.IsValueEquals(this.GetValue(model1, i), this.GetValue(model2, i)))
                    {
                        return false;
                    }
                }
            }
            else
            {
                return false;
            }

            return true;
        }
    }
}
